﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace FT.Architecture.Utils.ExtensionMethods
{
    public static class AppDomainExtensions
    {
        /// <summary>
        /// Returns all loaded and non-loaded assemblies that are directly or indirectly referenced by the given app domain.
        /// </summary>
        /// <param name="applicationDomain">Application Domain on which to perform the search</param>
        /// <returns></returns>
        public static IEnumerable<Assembly> GetLoadedAndReferencedAssemblies(this AppDomain applicationDomain)
        {
            return GetLoadedAndReferencedAssemblies(applicationDomain, name => true);
        }

        /// <summary>
        /// Returns all loaded and non-loaded assemblies that are directly or indirectly referenced by the given app domain.
        /// The results are then filtered using the given predicate.
        /// </summary>
        /// <param name="applicationDomain">Application Domain on which to perform the search</param>
        /// <param name="predicate">Only the assemblies matching this predicate will be returned</param>
        /// <returns></returns>
        public static IEnumerable<Assembly> GetLoadedAndReferencedAssemblies(this AppDomain applicationDomain, Func<AssemblyName, bool> predicate)
        {
            var loadedAssemblies = GetLoadedAssemblies(predicate);

            var found = new List<Assembly>();
            found.AddRange(loadedAssemblies);
            var alreadyVisited = new List<string>();
            alreadyVisited.AddRange(loadedAssemblies.Select(x => x.GetName().FullName));

            foreach (var loadedAssembly in loadedAssemblies)
            {
                LoadReferencedAssembliesRecursive(loadedAssembly, alreadyVisited, found, predicate);
            }

            return found;
        }

        private static IEnumerable<Assembly> GetLoadedAssemblies(Func<AssemblyName, bool> predicate)
        {
            var appDomain = AppDomain.CurrentDomain;
            var assemblies = from assembly
                             in appDomain.GetAssemblies()
                             where predicate(assembly.GetName())
                             select assembly;

            return assemblies;
        }

        private static void LoadReferencedAssembliesRecursive(Assembly assembly, List<string> alreadyVisited, List<Assembly> found, Func<AssemblyName, bool> predicate)
        {
            var referencedAssemblies = assembly.GetReferencedAssemblies();

            foreach (var referencedAssembly in referencedAssemblies)
            {
                if (alreadyVisited.Contains(referencedAssembly.FullName))
                {
                    continue;
                }

                if (predicate(referencedAssembly))
                {
                    try
                    {
                        var loadedAssembly = Assembly.Load(referencedAssembly);
                        found.Add(loadedAssembly);
                        alreadyVisited.Add(referencedAssembly.FullName);

                        LoadReferencedAssembliesRecursive(loadedAssembly, alreadyVisited, found, predicate);
                    }
                    catch (Exception)
                    {
                        Console.Out.WriteLine("Problem when loading {0}", referencedAssembly.Name);
                    }
                }
                else
                {
                    alreadyVisited.Add(referencedAssembly.FullName);
                }
            }
        }
    }
}
